package net.es.weathermap.metric;

import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

import org.json.JSONObject;
import org.json.JSONException;

import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.HttpVersion;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import org.apache.log4j.Logger;

public class HttpUtils {

    protected static HttpUtils instance;
    protected HttpClient client;
    protected MultiThreadedHttpConnectionManager connectionManager;
    private static Logger LOG = Logger.getLogger(ESnetMP.class);
    private HashMap<String, Integer> lastContacted;
    private HashMap<String, Integer> timeouts;

    public static HttpUtils getInstance() {
        if (instance == null) {
            instance = new HttpUtils();
        }
        return instance;
    }

    private HttpUtils() {
        lastContacted = new HashMap<String, Integer>();
        timeouts = new HashMap<String, Integer>();
        connectionManager = new MultiThreadedHttpConnectionManager();
        client = new HttpClient(connectionManager);
    }

    @SuppressWarnings("unchecked")
    public synchronized JSONObject
        getResponse(String key, List<String> serverUrls,
                    List<JSONObject> identifiers)
            throws JSONException {

        LOG.info("getResponse.start");
        Long nowL = System.currentTimeMillis()/1000;
        Integer now = nowL.intValue();

        JSONArray message = new JSONArray();
        for (JSONObject req : identifiers) {
            // LOG.info("id: "+id);
            message.put(req);
        }
        JSONObject response = null;
        for (String serverUrl : serverUrls) {
            LOG.info(serverUrl + " : metrics server");
            if (lastContacted.get(serverUrl) == null) {
                lastContacted.put(serverUrl, now);
            }
            if (timeouts.get(serverUrl) == null) {
                timeouts.put(serverUrl, 0);
            }
            Integer timeout = timeouts.get(serverUrl);
            Integer lastContact = lastContacted.get(serverUrl);

            // if true then this server still in timeout,
            // must try next one
            if (now - lastContact  < timeout) {
                LOG.info(serverUrl+" is still timed out");
                continue;
            }

            lastContacted.put(serverUrl, now);
            boolean error = false;
            try {
                response = this.sendAndReceive(key, message, serverUrl);
            } catch (HttpException e) {
                // A protocol exception.
                // log it - this should generally never happen
                LOG.error(e);
                e.printStackTrace();
                error = true;
            } catch (IOException e) {
                // a timeout or other possibly recoverable error
                LOG.info(e);
                e.printStackTrace();
                error = true;
            }
            if (response == null) {
                error = true;
            }
            if (error) {
                // 15 sec timeout to begin with
                // then, try next URL in list
                if (timeout == 0) {
                    timeout = 15;
                // double the timeout, up to 32 mins
                } else if (timeout < 1800) {
                    timeout = timeout * 2;
                }
                LOG.info(serverUrl+" : setting timeout to "+timeout);
                timeouts.put(serverUrl, timeout);
            } else {
                // reset timeout to 0 for this server
                // and don't try any other URLs
                timeouts.put(serverUrl, 0);
                break;
            }
        }
        LOG.info("getResponse.finish");
        return response;
    }

    public JSONObject
        sendAndReceive(String key, JSONArray message, String serverUrl)
            throws JSONException, HttpException, IOException  {

        LOG.info("sendAndReceive.start");
        PostMethod postMethod = new PostMethod(serverUrl);
        // postMethod.setRequestHeader("Content-Type", "text/plain");
        NameValuePair[] data = { new NameValuePair(key, message.toString(2)) };
        // LOG.info("key: " + key);
        // LOG.info("req: "+message.toString(2));

        postMethod.setRequestBody(data);
        postMethod.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
        postMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler(3, true));

        client.getParams().setParameter("http.connection.stalecheck", true);
        client.getParams().setParameter("http.protocol.version", HttpVersion.HTTP_1_0);
        client.executeMethod(postMethod);
        int statusCode = postMethod.getStatusCode();
        if (statusCode != HttpStatus.SC_OK ) {
            // LOG.info("sendAndReceive: 1st status not ok: "+statusCode);
            // LOG.info("req: "+message.toString(2));
            // retry once more
            client.executeMethod(postMethod);
            if (statusCode != HttpStatus.SC_OK ) {
                postMethod.releaseConnection();
                LOG.info("sendAndReceive: bad http status: "+statusCode);
                throw new HttpException("bad http status: "+statusCode);
            }
        }
        InputStream stream = postMethod.getResponseBodyAsStream();
        String resp = convertStreamToString(stream);
        LOG.info(resp);
        JSONObject responseMessage = new JSONObject(resp);
        postMethod.releaseConnection();
        LOG.info("sendAndReceive.finish");
        return responseMessage;
    }

    public String convertStreamToString(InputStream is) {
        /*
         * To convert the InputStream to String we use the BufferedReader.readLine()
         * method. We iterate until the BufferedReader return null which means
         * there's no more data to read. Each line will appended to a StringBuilder
         * and returned as String.
         */
        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
        StringBuilder sb = new StringBuilder();
        String line = null;
        try {
            while ((line = reader.readLine()) != null) {
                sb.append(line + "\n");
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return sb.toString();
    }
}
